/**
 * @(#)XmlHelper.java Oct 7, 2007
 *
 * Copyright 2007 Softao Corporation. All rights reserved.
 * SOFTAO PROPRITERY/ CONFEDENTIAL. Use is subject to license terms.
 */
package com.softao.ming.gde.internal.core.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Provides help methods for processing the XML related data.
 * 
 * @author dop_sun
 * @version 1.0.0 Oct 7, 2007
 * @since 1.0.0
 */
public final class XmlHelper {
	private static final String mXsltFile;

	static {
		String name = XmlHelper.class.getName();
		name = name.replace(".", "/");
		name = name.replace("XmlHelper", "XmlPrettyPrint.xslt");
		mXsltFile = name;
	}
	
	/**
	 * Creates the document with the specified the <code>rootTagName</code>.
	 * <p>
	 * The returned document is:
	 * </p>
	 * 
	 * <pre>
	 * &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;
	 * &lt;rootTagName xmlns=&quot;schemaUrl&quot; &gt;
	 *   
	 * &lt;/rootTagName&gt;
	 * </pre>
	 * 
	 * @param schemaURL
	 *            the URL of the schema. Ignored if is <code>null</code>.
	 * @param rootTagName
	 *            the root tag name. This can not be <code>null</code> or empty
	 *            string.
	 * @return the {@link Document} instance
	 */
	public static Document createDocument(String schemaURL, String rootTagName) {
		if ((rootTagName == null) || (rootTagName.length() == 0))
			throw new NullPointerException(
					"rootTagName can not be null or empty string.");

		StringBuilder sb = new StringBuilder();
		sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
		sb.append("<");
		sb.append(rootTagName);
		if (schemaURL != null) {
			sb.append(" xmlns=\"");
			sb.append(schemaURL);
			sb.append("\"");
		}
		sb.append(">\r\n");
		sb.append("</");
		sb.append(rootTagName);
		sb.append(">\r\n");

		try {
			return XmlHelper.loadDocument(sb.toString());
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Returns an iterator over the children of the given element with the given
	 * <code>tagName</code>.
	 * 
	 * @param element
	 *            the parent element
	 * @param tagName
	 *            the name of the desired child
	 * @return an iterator of children
	 */
	public static Iterator<Element> getChildren(Element element, String tagName) {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");
		if ((tagName == null) || (tagName.length() == 0))
			throw new NullPointerException(
					"tagName can not be null or empty string.");

		List<Element> goodChildren = new ArrayList<Element>();

		NodeList children = element.getChildNodes();
		for (int i = 0; i < children.getLength(); i++) {
			Node currentChild = children.item(i);
			if (currentChild.getNodeType() == Node.ELEMENT_NODE
					&& ((Element) currentChild).getTagName().equals(tagName)) {
				goodChildren.add((Element) currentChild);
			}
		}
		return goodChildren.iterator();
	}

	/**
	 * An alias for {@link #getOptionalChild(Element, String)}.
	 * 
	 * @param element
	 * @param tagName
	 * @return the optional element
	 * @throws IOException
	 */
	public static Element getOc(Element element, String tagName)
			throws IOException {
		return getOptionalChild(element, tagName);
	}

	/**
	 * An alias for {@link #getOptionalChild(Element, String, Element)}
	 * 
	 * @param element
	 * @param tagName
	 * @param defaultElement
	 * @return the optional element
	 * @throws IOException
	 */
	public static Element getOc(Element element, String tagName,
			Element defaultElement) throws IOException {
		return getOptionalChild(element, tagName, defaultElement);
	}

	/**
	 * Returns the child with the specified <code>tagName</code>. If the child
	 * does not exist, then <code>null</code> returned.
	 * <p>
	 * This method is same as
	 * <code>getOptionalChild(element, tagName, null)</code>.
	 * </p>
	 * 
	 * @param element
	 *            the parent node
	 * @param tagName
	 *            the tag name
	 * @return the child with the specific name, <code>null</code> if the node
	 *         does not exist.
	 * @throws IOException
	 *             throws if there are more than 1 element with the
	 *             <code>tagName</code>.
	 */
	public static Element getOptionalChild(Element element, String tagName)
			throws IOException {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");
		if ((tagName == null) || (tagName.length() == 0))
			throw new NullPointerException(
					"tagName can not be null or empty string.");

		return XmlHelper.getOptionalChild(element, tagName, null);
	}

	/**
	 * Returns the child with the specified <code>tagName</code>. If the child
	 * does not exist, then <code>defaultElement</code> returned.
	 * <p>
	 * In this context, only one child of the <code>element</code> with
	 * specified <code>tagName</code> is expected. If multiple children exist,
	 * {@link IOException} thrown. Different from
	 * {@link #getOptionalChild(Element, String)}, if the child does not exist,
	 * returns <code>defaultElement</code>.
	 * </p>
	 * 
	 * @param element
	 *            the parent element
	 * @param tagName
	 *            the name of the child
	 * @param defaultElement
	 *            the default element if the child does not exist
	 * @return the child with the specific name, or <code>defaultElement</code>
	 *         returned if not exist.
	 * @throws IOException
	 *             throws if there are more than 1 element with the specified
	 *             <code>tagName</code>
	 */
	public static Element getOptionalChild(Element element, String tagName,
			Element defaultElement) throws IOException {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");
		if ((tagName == null) || (tagName.length() == 0))
			throw new NullPointerException(
					"tagName can not be null or empty string.");

		Iterator<Element> children = XmlHelper.getChildren(element, tagName);

		if ((children != null) && (children.hasNext())) {
			Element result = children.next();

			if (children.hasNext()) {
				throw new IOException("Expected only 1 child, "
						+ "but actually has more.");
			}

			return result;
		} else {
			return defaultElement;
		}
	}

	/**
	 * An alias for {@link #getUniqueChild(Element, String)}.
	 * 
	 * @param element
	 * @param tagName
	 * @return the unique child
	 * @throws IOException
	 */
	public static Element getUc(Element element, String tagName)
			throws IOException {
		return getUniqueChild(element, tagName);
	}

	/**
	 * Gets the unique child element under the <code>element</code>.
	 * <p>
	 * In this context, one and only one child of the <code>element</code> with
	 * the specified <code>tagName</code> is expected. If the child does not
	 * exist, or multiple children exist, {@link IOException} thrown.
	 * </p>
	 * 
	 * @param element
	 *            the parent element
	 * @param tagName
	 *            the name of the unique child
	 * @return the child with the name as <code>tagName</code>
	 * 
	 * @throws IOException
	 *             throws while there is zero element, or more than one element
	 *             meeting the criteria.
	 */
	public static Element getUniqueChild(Element element, String tagName)
			throws IOException {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");
		if ((tagName == null) || (tagName.length() == 0))
			throw new NullPointerException(
					"tagName can not be null or empty string.");

		Iterator<Element> children = XmlHelper.getChildren(element, tagName);
		if (children.hasNext()) {
			Element result = children.next();
			if (children.hasNext()) {
				throw new IOException("Expected only 1 child, "
						+ "but actually has more.");
			}
			return result;
		} else {
			throw new IOException("Excpected 1 child, but not exists");
		}
	}

	/**
	 * Loads the document from the <code>content</code>.
	 * 
	 * @param content
	 *            the content to load
	 * @return the {@link Document} represents the DOM of the content
	 * @throws IOException
	 */
	public static Document loadDocument(String content) throws IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			return builder.parse(new InputSource(new StringReader(content)));
		} catch (ParserConfigurationException e) {
			throw new IOException("An internal operation failed.", e);
		} catch (SAXException e) {
			throw new IOException("An internal operation failed.", e);
		}
	}
	
	/**
	 * @param inputStream
	 * @return document to read
	 * @throws IOException
	 */
	public static Document loadDocument(InputStream inputStream) throws IOException {
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			return builder.parse(new InputSource(inputStream));
		} catch (ParserConfigurationException e) {
			throw new IOException("An internal operation failed.", e);
		} catch (SAXException e) {
			throw new IOException("An internal operation failed.", e);
		}
	}

	/**
	 * Loads document from the <code>url</code>.
	 * 
	 * @param uri
	 *            the location of the document
	 * @return the instance of the document loaded
	 * @throws IOException
	 * 
	 */
	public static Document loadDocument(URL uri) throws IOException {
		if (uri == null)
			throw new NullPointerException("uri can not be null reference.");

		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		try {
			DocumentBuilder builder = factory.newDocumentBuilder();
			return builder.parse(uri.toString());
		} catch (ParserConfigurationException e) {
			throw new IOException("An internal operation failed.", e);
		} catch (SAXException e) {
			throw new IOException("An internal operation failed.", e);
		}
	}

	/**
	 * Reads the content of the <code>document</code> (UTF-8 only).
	 * 
	 * @param document
	 *            the document to be read
	 * @return the content of this document
	 * @throws IOException
	 */
	public static final String readContent(Document document)
			throws IOException {
		Writer writer = new StringWriter();
		try {
			InputStream xsltStream = XmlHelper.class.getClassLoader().getResourceAsStream(
					mXsltFile);
			try {
				StreamSource styleSource = new StreamSource(xsltStream);

				try {
					DOMSource domSource = new DOMSource(document);
					StreamResult streamResult = new StreamResult(writer);
					TransformerFactory tf = TransformerFactory.newInstance();
					Transformer serializer = tf.newTransformer(styleSource);
					serializer.transform(domSource, streamResult);
					writer.flush();
					return writer.toString();
				} finally {
					writer.close();
				}
			} finally {
				xsltStream.close();
			}
		} catch (TransformerException ex) {
			throw new IOException("Read content failed.", ex);
		} catch (IOException ex) {
			throw new IOException("Read content failed.", ex);
		}
	}

	/**
	 * Returns the content of the element.
	 * <p>
	 * If <code>element</code> is <code>null</code>, <code>null</code> returned.
	 * </p>
	 * 
	 * @param element
	 *            the element
	 * @return the content of the <code>content</code>
	 */
	public static String readContent(Element element) {
		return XmlHelper.readContent(element, null);
	}

	/**
	 * Gets the content of the <code>element</code>. Different from
	 * {@link #readContent(Element)}, if <code>element</code> is
	 * <code>null</code>, <code>defaultContent</code> returned.
	 * 
	 * @param element
	 *            the element to check
	 * @param defaultContent
	 *            the default content returned if <code>element</code> is
	 *            <code>null</code>
	 * @return If <code>element</code> is <code>null</code>,
	 *         <code>defaultContent</code> returned; otherwise, the content of
	 *         <code>element</code> returned.
	 */
	public static String readContent(Element element, String defaultContent) {
		if (element == null) {
			return defaultContent;
		}

		NodeList children = element.getChildNodes();
		StringBuilder result = new StringBuilder("");
		for (int i = 0; i < children.getLength(); i++) {
			if (children.item(i).getNodeType() == Node.TEXT_NODE
					|| children.item(i).getNodeType() == Node.CDATA_SECTION_NODE) {
				result.append(children.item(i).getNodeValue());
			}
		}
		return result.toString().trim();
	}

	/**
	 * An alias for {@link #readOptionalChildContent(Element, String)}.
	 * 
	 * @param element
	 * @param tagName
	 * @return the content of optional child of the <code>element</code>
	 * @throws IOException
	 */
	public static String readOcc(Element element, String tagName)
			throws IOException {
		return readOptionalChildContent(element, tagName);
	}

	/**
	 * Gets the content of the optional child of the <code>element</code>.
	 * <p>
	 * This method internally called {@link #getOptionalChild(Element, String)}
	 * to get the optional child of <code>element</code>.
	 * 
	 * @param element
	 *            the parent element. Refer
	 *            {@link #getOptionalChild(Element, String)} for details.
	 * @param tagName
	 *            the tag name. Refer {@link #getOptionalChild(Element, String)}
	 *            for details.
	 * @return the content of unique child of the <code>element</code>
	 * @throws IOException
	 * 
	 * @see #getOptionalChild(Element, String)
	 */
	public static String readOptionalChildContent(Element element,
			String tagName) throws IOException {
		Element child = XmlHelper.getOptionalChild(element, tagName);
		return XmlHelper.readContent(child);
	}

	/**
	 * An alias for {@link #readUniqueChildContent(Element, String)}.
	 * 
	 * @param element
	 * @param tagName
	 * @return the content of unique child of the <code>element</code>
	 * @throws IOException
	 */
	public static String readUcc(Element element, String tagName)
			throws IOException {
		return readUniqueChildContent(element, tagName);
	}

	/**
	 * Gets the content of the unique child of the <code>element</code>.
	 * <p>
	 * This method internally called {@link #getUniqueChild(Element, String)} to
	 * get the unique child of <code>element</code>.
	 * 
	 * @param element
	 *            the parent element. Refer
	 *            {@link #getUniqueChild(Element, String)} for details.
	 * @param tagName
	 *            the tag name. Refer {@link #getUniqueChild(Element, String)}
	 *            for details.
	 * @return the content of unique child of the <code>element</code>
	 * @throws IOException
	 * 
	 * @see #getUniqueChild(Element, String)
	 */
	public static String readUniqueChildContent(Element element, String tagName)
			throws IOException {
		Element child = XmlHelper.getUniqueChild(element, tagName);
		return XmlHelper.readContent(child);
	}

	/**
	 * Writes the content as CData section.
	 * 
	 * @param element
	 * @param value
	 */
	public static final void writeCData(Element element, Object value) {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");

		while (element.getChildNodes().getLength() > 0) {
			element.removeChild(element.getFirstChild());
		}

		if (value != null) {
			Text text = element.getOwnerDocument().createCDATASection(
					value.toString());
			element.appendChild(text);
		}
	}

	/**
	 * Sets the child content. If the <code>value</code> is <code>null</code>,
	 * the child node specified by <code>tagName</code> will be removed.
	 * 
	 * @param element
	 *            the element
	 * @param tagName
	 *            the tag name
	 * @param value
	 *            the value of the child element
	 * @throws IOException
	 *             throws while more than 1 child element with the same
	 *             <code>tagName</code> exist.
	 */
	public static final void writeCData(Element element, String tagName,
			Object value) throws IOException {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");
		if ((tagName == null) || (tagName.length() == 0))
			throw new NullPointerException(
					"tagName can not be null or empty string.");

		Element childElement = XmlHelper.getOptionalChild(element, tagName);
		if (value == null) {
			if (childElement != null) {
				element.removeChild(childElement);
			}
		} else {
			if (childElement == null) {
				childElement = element.getOwnerDocument().createElement(tagName);
				element.appendChild(childElement);
			}

			XmlHelper.writeCData(childElement, value);
		}
	}

	/**
	 * writes <code>value</code> as the text content of <code>element</code>.
	 * The existing content of the <code>element</code> will be replaced if
	 * exist.
	 * 
	 * @param element
	 *            the element to write
	 * @param value
	 *            the value of the element. Remove the content if the
	 *            <code>value</code> is <code>null</code>.
	 */
	public static final void writeText(Element element, Object value) {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");

		while (element.getChildNodes().getLength() > 0) {
			element.removeChild(element.getFirstChild());
		}

		if (value != null) {
			Text text = element.getOwnerDocument().createTextNode(
					value.toString());
			element.appendChild(text);
		}
	}

	/**
	 * Sets the child content. If the <code>value</code> is <code>null</code>,
	 * the child node specified by <code>tagName</code> will be removed.
	 * 
	 * @param element
	 *            the element
	 * @param tagName
	 *            the tag name
	 * @param value
	 *            the value of the child element
	 * @throws IOException
	 *             throws while more than 1 child element with the same
	 *             <code>tagName</code> exist.
	 */
	public static final void writeText(Element element, String tagName,
			Object value) throws IOException {
		if (element == null)
			throw new NullPointerException("element can not be null reference.");
		if ((tagName == null) || (tagName.length() == 0))
			throw new NullPointerException(
					"tagName can not be null or empty string.");

		Element childElement = XmlHelper.getOptionalChild(element, tagName);
		if (value == null) {
			if (childElement != null) {
				element.removeChild(childElement);
			}
		} else {
			if (childElement == null) {
				childElement = element.getOwnerDocument().createElement(tagName);
				element.appendChild(childElement);
			}

			XmlHelper.writeText(childElement, value);
		}
	}

	/**
	 * Prevents <code>XmlHelper</code> to be initialized for it's a pure static
	 * class.
	 */
	private XmlHelper() {

	}

}
